using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;

namespace Limestone.Storage.XML
{
    public class LookUpProvider
    {
        private readonly IFileReader _fileReader;
        private readonly IXmlStorageConfig _xmlStorageConfig;
        private XmlReaderSettings _xmlReadSettings;

        public LookUpProvider(IFileReader fileReader, IXmlStorageConfig xmlStorageConfig)
        {
            _fileReader = fileReader;
            _xmlStorageConfig = xmlStorageConfig;

           _xmlReadSettings = new XmlReaderSettings
            {
                ConformanceLevel = ConformanceLevel.Fragment,
                IgnoreWhitespace = true,
                IgnoreComments = true
            };
        }

        public string CacheFilePath { get { return Path.Combine(_xmlStorageConfig.BasePagePath, @"Limestone.LookUpCache"); } }

        public IPageLookUp GetPageLookUp(bool forceDeepReload)
        {
            if (forceDeepReload || !File.Exists(CacheFilePath))
            {
                RebuildNavigationConfig();
            }

            var pages = new Dictionary<string, IPageStub>(StringComparer.OrdinalIgnoreCase);
            var byParentPath = new Dictionary<string, IEnumerable<IPageStub>>(StringComparer.OrdinalIgnoreCase);

            var xmlDoc = LoadCacheFile();


            // TODO switch to an xmlreader to avoid in memory operations like now
            var pageNodes = xmlDoc.GetElementsByTagName("Page");

            foreach (XmlNode xPage in pageNodes)
            {
                if (xPage.Attributes == null) continue;
                
                var path = xPage.Attributes["Path"].Value;

                var stub = new PageStub
                               {
                                   Name = xPage.Attributes["Name"].Value,
                                   Path = path,
                                   Title = xPage.Attributes["Title"].Value,
                                   TemplatePath = xPage.Attributes["TemplatePath"].Value,
                                   Hidden = (bool.Parse(xPage.Attributes["Hidden"].Value))
                               };


                pages.Add(path, stub);

                // the root page should be added to the byParentId list
                if (stub.Path == "/")
                    continue;

                var parentPath = "/";

                // not root pages
                if (path.LastIndexOf("/", StringComparison.Ordinal) > 0)
                {
                    // identify index-pages
                    if (stub.Name == _xmlStorageConfig.FolderIndexPageName)
                    {
                        parentPath = path.Substring(0, path.LastIndexOf("/", StringComparison.Ordinal));
                        var idx = parentPath.LastIndexOf("/", StringComparison.Ordinal);
                        if (idx == 0)
                            parentPath = "/";
                        else
                            parentPath = (parentPath.Substring(0, idx + 1) + _xmlStorageConfig.FolderIndexPageName + ".page").ToLower();
                    }
                    else
                        parentPath = PageHelper.GetParentPath(path);
                }

                if (byParentPath.ContainsKey(parentPath))
                    byParentPath[parentPath] = byParentPath[parentPath].Concat(new[] { stub });
                else
                {
                    byParentPath.Add(parentPath, new[] { stub });
                }
            }
            return new PageLookUp
                       {
                           Pages = pages,
                           PagesByParentId = byParentPath
                       };
        }

        private XmlDocument LoadCacheFile()
        {
            var xmlDoc = new XmlDocument();
            var attempts = 5;
            Exception cannotReadException = null;
            while (attempts > 0)
            {
                try
                {
                    using (var fileStream = new FileStream(CacheFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                    {
                        xmlDoc.Load(fileStream);
                        attempts = 0;
                    }
                }
                catch (Exception exception)
                {
                    cannotReadException = exception;
                    System.Threading.Thread.Sleep(100);
                    attempts--;
                }
            }

            if (cannotReadException != null)
            {
                throw cannotReadException;
            }
            return xmlDoc;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="stub">The page stub to add</param>
        /// <param name="parentPath">The path to the parent page</param>
        public void AddPageStub(IPageStub stub, string parentPath)
        {
            // load the XML document
            var navDoc = new XmlDocument();

            using (var fileStream = new FileStream(CacheFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
            {
                navDoc.Load(fileStream);
            }

            // create a new node of the stub
            var xmlStub = navDoc.CreateElement("Page");
            AddXmlAttribute(navDoc, xmlStub, "Title", stub.Title);
            AddXmlAttribute(navDoc, xmlStub, "Name", stub.Name);
            AddXmlAttribute(navDoc, xmlStub, "Path", stub.Path);
            AddXmlAttribute(navDoc, xmlStub, "TemplatePath", stub.TemplatePath);
            AddXmlAttribute(navDoc, xmlStub, "Hidden", (stub.Hidden) ? "true" : "false");

            var parentNode = EnsureNode(navDoc, parentPath);
            if (parentNode == null)
                return;

            parentNode.AppendChild(xmlStub);

            // write the document back to the file system
            using (TextWriter sw = new StreamWriter(CacheFilePath, false, Encoding.UTF8))
            {
                navDoc.Save(sw);
            }
        }

        private XmlNode EnsureNode(XmlDocument navDoc, string path)
        {
            var node = navDoc.SelectSingleNode("//Page[@Path='" + path + "']");
            if (node != null)
                return node;

            var name = path.Substring(path.LastIndexOf(@"/") + 1);

            // create a new node of the stub
            var xmlStub = navDoc.CreateElement("Page");
            AddXmlAttribute(navDoc, xmlStub, "Title", name);
            AddXmlAttribute(navDoc, xmlStub, "Name", name);
            AddXmlAttribute(navDoc, xmlStub, "Path", path);
            AddXmlAttribute(navDoc, xmlStub, "TemplatePath", "");
            AddXmlAttribute(navDoc, xmlStub, "Hidden", "false");

            var parentPath = PageHelper.GetParentPath(path);

            //var parentNode = parentPath == "/" ? navDoc.SelectSingleNode("//LookUpCache") : EnsureNode(navDoc, parentPath);
            var parentNode = EnsureNode(navDoc, parentPath);

            // add the new node
            return parentNode.AppendChild(xmlStub);
        }

        private static void AddXmlAttribute(XmlDocument navDoc, XmlElement xmlStub, string attributeName, string attributeValue)
        {
            var attr = navDoc.CreateAttribute(attributeName);
            attr.Value = attributeValue;
            xmlStub.Attributes.Append(attr);
        }

        public void RemovePageStub(string path)
        {
            // load the XML document
            var navDoc = new XmlDocument();
            using (var fileStream = new FileStream(CacheFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
            {
                navDoc.Load(fileStream);
            }

            // find the element(s) we want to remove, using an XPath query
            var node = navDoc.SelectSingleNode("//Page[@Path='" + path + "']");
            if (node == null || node.ParentNode == null)
                return;

            node.ParentNode.RemoveChild(node);

            // write the document back to the file system
            using (TextWriter sw = new StreamWriter(CacheFilePath, false, Encoding.UTF8))
            {
                navDoc.Save(sw);
            }
        }

        public void MovePageStub(string oldPath, string newParentPath)
        {
            // load the XML document
            var navDoc = new XmlDocument();
            using (var fileStream = new FileStream(CacheFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
            {
                navDoc.Load(fileStream);
            }
            // find the element(s) we want to remove, using an XPath query
            var node = navDoc.SelectSingleNode("//Page[@Path='" + oldPath + "']");
            var newParentNode = navDoc.SelectSingleNode("//Page[@Path='" + newParentPath + "']");

            if (node == null || newParentNode == null)
                return;

            newParentNode.AppendChild(node.CloneNode(true));

            node.ParentNode.RemoveChild(node);

            // write the document back to the file system
            using (TextWriter sw = new StreamWriter(CacheFilePath, false, Encoding.UTF8))
            {
                navDoc.Save(sw);
            }
        }

        // TODO consider speeding up by perhaps this one http://www.codeproject.com/KB/files/FastDirectoryEnumerator.aspx
        // or possible a deep search http://msdn.microsoft.com/en-us/library/ms143448%28VS.85%29.aspx
        /// <summary>
        /// This is a resource consuming process. Do not call it unless you know what you do
        /// </summary>
        /// <returns></returns>
        public bool RebuildNavigationConfig()
        {

            var writeSettings = new XmlWriterSettings { Indent = true, Encoding = Encoding.UTF8 };

            var writer = XmlWriter.Create(CacheFilePath, writeSettings);

            writer.WriteStartDocument();

            writer.WriteStartElement("Page");
            writer.WriteAttributeString("CreationTime", DateTime.Now.ToShortDateString());
            writer.WriteAttributeString("Path", "/");
            writer.WriteAttributeString("Name", "Start");
            writer.WriteAttributeString("Title", "Start");
            writer.WriteAttributeString("TemplatePath", "Start");
            writer.WriteAttributeString("Hidden", "false");

            WriteNavigationXmlForDirectory(_xmlStorageConfig.BasePagePath, ref writer);

            writer.WriteEndElement();
            writer.WriteEndDocument();
            writer.Close();

            return true;
        }

        private void WriteNavigationXmlForDirectory(string directoryPath, ref XmlWriter writer)
        {
            // possible sub pages and sub directories
            var di = new DirectoryInfo(directoryPath);
            var files = di.GetFiles("*.page");
            var directories = di.GetDirectories();

            // .page-files 
            foreach (var fileInfo in files.Where(fileInfo => Path.GetFileNameWithoutExtension(fileInfo.FullName).ToLower() != _xmlStorageConfig.FolderIndexPageName.ToLower()))
            {
                WritePageStubXMLFromFile(ref writer, fileInfo.FullName, true);
            }

            // directories 
            foreach (var directoryInfo in directories)
            {
                // begin page element
                WritePageStubXMLFromFile(ref writer, Path.Combine(directoryInfo.FullName, _xmlStorageConfig.FolderIndexPageName + ".page"), false);

                writer.WriteStartElement("Pages");

                WriteNavigationXmlForDirectory( directoryInfo.FullName, ref writer);

                // end pages element
                writer.WriteEndElement();

                // end page element
                writer.WriteEndElement();
            }
        }

        private void WritePageStubXMLFromFile(ref XmlWriter writer, string filePath, bool closeElement)
        {
            var stub = _fileReader.ReadPageFile(filePath);

            if (stub == null)
            {
                stub = new PageStub()
                {
                    Name = Path.GetFileNameWithoutExtension(filePath),
                    Path = filePath.Substring(_xmlStorageConfig.BasePagePath.Length).Replace(@"\", "/"),
                    Title = Path.GetFileNameWithoutExtension(filePath),
                    TemplatePath = "",
                    Hidden = false,
                }; 
            }

            // write to the cache file
            WritePageXml(writer, stub);

            if (closeElement)
                writer.WriteEndElement();
        }

        private static void WritePageXml(XmlWriter writer, IPageStub stub)
        {
            writer.WriteStartElement("Page");
            writer.WriteAttributeString("Name", stub.Name);
            writer.WriteAttributeString("Path", stub.Path);
            writer.WriteAttributeString("Title", stub.Title);
            writer.WriteAttributeString("TemplatePath", stub.TemplatePath);
            writer.WriteAttributeString("Hidden", stub.Hidden.ToString());
        }


    }
}